import os, sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/python_libs')

import bottle, xlrd, uuid, hashlib, re, json, random, string, tempfile
from datetime import datetime, date, timedelta
from collections import defaultdict
from bottle import route, post, request, response, static_file, redirect

import datastore as db
import protocol


#
# ROUTES FOR STATIC FILES
#

@route('/js/<file:path>')
def get_script(file): return static_file(file, root='js')

@route('/style/<file:path>')
def get_style(file): return static_file(file, root='style')

@route('/images/<file:path>')
def get_image(file): return static_file(file, root='images')

@route('/data/upload/<file:path>')
def get_file(file): return static_file(file, root='data/upload')

@route('/data/protocols/<filename>')
def get_protocol(filename):
    return static_file(filename, root='data/protocols',
        mimetype='application/js')



#
# START PAGE
#

@route('/')
def index():
    if current_user() == None:
        redirect('/login')
    response = static_file('index.html', root='static')
    response.set_header('Cache-Control', 'no-cache')
    return response



#
# USER AUTHENTICATION
#

sessions = {}

@route('/login')
def login():
	# Clear existing sessions
	key = request.get_cookie('session', '')
	sessions = db.search('sessions', 'key = %s' % key)
	for s in sessions: db.update(s['id'], {'key': '#'})

	response = static_file('login.html', root='static')
	response.set_header('Cache-Control', 'no-cache')
	return response

@post('/login_user')
def login_user():
    form = request.forms
    return { 'success':
    	authenticate(form.get('email'), form.get('password')) }

@post('/register_user')
def register_user():
    form = request.forms
    add_user(form.get('full_name'), form.get('email'), form.get('password'))
    return {'success': authenticate(form.get('email'), form.get('password'))}

@post('/api/create_group')
def api_create_group():
	form = request.forms
	group_id = add_group(form.get('group_name'))
	return {'success': user_join_group(current_user()['id'], groups[0]['id'])}

@post('/api/join_group')
def api_join_group():
	form = request.forms
	groups = db.search('groups', 'password = %s' % form.get('password'))
	if len(groups) != 1: return {'success': False}
	return {'success': user_join_group(current_user()['id'], groups[0]['id']),
		'group_name': groups[0]['name']}

public_protocols = 0

def salt_password(plain, salt):
    return hashlib.sha1(plain.encode('utf-8')).hexdigest()

def authenticate(email, password):
    user = db.search('users', 'email = %s' % email)
    if len(user) != 1: return False
    user = user[0]

    if salt_password(password, user['salt']) != user['hash']:
        return False

    expiry_days = 180
    key = uuid.uuid4().hex
    sessions[key] = user['id']
    response.set_cookie('session', key, max_age=expiry_days * 24 * 3600)
    return True

def current_user():
    key = request.get_cookie('session', '')
    if not key in sessions: return None
    user = db.get(sessions[key])
    user['groups'] = [int(g) for g in user['groups'].split(',') if g]
    user['protocols'] = [int(p) for p in user['protocols'].split(',') if p]
    return user

def add_user(name, email, password):
    salt = uuid.uuid4().hex
    user_id = db.insert('users', {'name': name, 'email': email,
        'hash': salt_password(password, salt), 'salt': salt,
        'groups': str(public_protocols), 'protocols': ''})
    user_join_group(user_id, user_id)
    return user_id

def add_group(name):
    password = ''.join(random.choice(string.ascii_letters + string.digits)
        for x in range(8))
    print('Password for group "%s" is "%s".' % (name, password))
    return db.insert('groups', {'name': name, 'description': '',
        'password': password})

def get_group_names(ids):
    return [db.get(int(id))['name'] for id in ids]

def user_join_group(user_id, group):
    groups = [g for g in db.get(user_id)['groups'].split(',')]
    if str(group) in groups: return False
    db.update(user_id, {'groups': ','.join(groups + [str(group)])})
    return True

def user_add_protocol(user_id, protocol):
    user_protocols = [p for p in db.get(user_id)['protocols'].split(',') if p]
    if not type(protocol) == str:
        protocol = str(protocol)
    if not protocol in user_protocols:
        db.update(user_id, {'protocols': ','.join(user_protocols + [protocol])})

def user_delete_protocol(user_id, protocol):
    user_protocols = [p for p in db.get(user_id)['protocols'].split(',') if p]
    if not type(protocol) == str:
        protocol = str(protocol)
    user_protocols.remove(protocol);
    db.update(user_id, {'protocols': ','.join(user_protocols)})

@route('/get_user_info')
def get_user_info():
	user = current_user()
	group_names = [db.get(id)['name'] for id in user['groups']]
	protocol_names = []
	for id in user['protocols']:
		protocol = db.get(id)
		protocol_names.append({'name':protocol['Name'], 'id': id, 'type': protocol['Protocol.Type']})

	group_updates = {}
	for group_name, group in zip(group_names, user['groups']):
		news = group_updates.setdefault(group_name, [])
		for change in db.latest_changes(str(group)):
			who = db.get(change[0]).get('name', 'Unknown user')
			deeds = []
			if change[2] == 1: deeds.append('added 1 item')
			if change[2] > 1: deeds.append('added %d items' % change[2])
			if change[3] == 1: deeds.append('modified 1 item')
			if change[3] > 1: deeds.append('modified %d items' % change[3])
			news.append('%s - %s %s.' % (change[1], who, ' and '.join(deeds)))

	experiments = get_experiments()
	return {
		'user': {
			'name': user['name'], 
			'email': user['email']
		}, 
		'groups': group_names, 
		'experiments': experiments,
		'protocols': protocol_names,
		'group_updates': group_updates
	}



#
# ITEMS
#

@route('/search/<query:re:.*>')
def search(query):
	# First we require that the items must be visible to the user.
    # Supported tokens include plain words and queries such as
    # [diagnostic sample = yes].
    groups = current_user()['groups']

    tokens = re.findall(r'[^\s\[\]]+|\[.+?\]', query)
    plain_terms = [t for t in tokens if not t.startswith('[')]
    attrib_terms = [t for t in tokens if t.startswith('[')]
    
    # Plain term search.
    queries = plain_terms
    
    # Attribute-specific search.
    queries += [t[1:-1] for t in attrib_terms]
        
    # Perform search.
    items = []
    for group in groups:
        items += db.search(str(group), queries, case=False, deep=True)
        
    # Partition the items based on type.
    print('Partitioning items...')
    items_by_type = defaultdict(list)
    for item in items:
        try:
            items_by_type[item['Type']].append(item)
        except:
            print(item)
        
    print('Items partitioned by type.')
    return items_by_type


@post('/update')
def update_item():
    data = request.json
    added = 0
    modded = 0
    print(data)
    for key in data:
        if not 'sample_ids' in data[key]:
            table = str(current_user()['id'])
            keys = list(data[key]['changes'].keys())
            items = [list(data[key]['changes'].values())]*int(data[key]['amount'])
            merge_keys = []
            for idx in range(0, len(keys)):
                if keys[idx][0] == '%':
                    merge_keys.append(keys[idx][1:])
                    keys[idx] = keys[idx][1:]

            print(keys)
            print(merge_keys)
            db.insert_multiple(str(table), keys, items, merge=merge_keys, edit=int(table))
            added += int(data[key]['amount'])
        else:
            for id in data[key]['sample_ids']:
                db.update(id, data[key]['changes'])
                modded += 1
    return {'success': True, 'added': added, 'modded': modded}


@post('/update_experiment')
def update_experiment():
    db.update(int(request.forms['id']), {
    	'Experiment.json': request.forms['json']})
    return {'success': True}


@post('/finish_experiment')
def finish_experiment():
    print (request.json)
    data = request.json
    added = 0
    modified = 0
    for key in data:
        if key == 'add':
            for t in data[key]:
                for i in data[key][t]:
                    db.insert(str(current_user()['id']), i);
                    added += 1
        elif key == 'mod':
            for t in data[key]:
                for i in data[key][t]:
                    item_id = int(i.pop('id'))
                    print (i)
                    db.update(item_id, i)
                    modified += 1

    db.update(data['id'], {'State': 'COMPLETE','Finished': datetime.now().strftime("%d.%m.%Y %H:%M:%S")})
    return {'success': True, 'added': added, 'modified': modified}


@route('/delete_experiment/<id>')
def delete_experiment(id):
    return {'success': True}   # FIXME


@post('/add_protocol')
def _add_protocol():
    proto = request.json
    user_id = current_user()['id']
    add_protocol(proto['name'], proto['description'],proto['settings'], proto['instructions'], proto['type'], user_id)
    return {'success': True, 'added': {'name':proto['name']}}

@post('/add_experiment')
def _add_experiment():
    print (request.forms['json']);
    id = add_experiment(request.forms['protocol_id'], request.forms['protocol'], request.forms['json'], user=current_user())
    return {'success': True, 'id': id}

@route('/get_experiment_json/<id>')
def _get_experiment_json(id):
    experiment = get_experiment_json(int(id))
    print (experiment)
    return {'json':experiment, 'success': True}

@route('/user_add_protocol/<id>')
def _user_add_protocol(id):
    user = current_user()['id'];
    user_add_protocol(user, id)
    protocol = db.get(int(id))

    return {'success': True, 'added': {'name':protocol['Name'], 'id': id, 'type': protocol['Protocol.Type']}}

@route('/user_delete_protocol/<id>')
def _user_delete_protocol(id):
    user = current_user()['id'];
    user_delete_protocol(user, id)

@post('/upload_excel')
def upload_excel():
    if(request.forms['type'] == 'analyze'):
        excel = request.files.get('excel')
        name, ext = os.path.splitext(excel.filename)
        print(name,ext)
        if ext.lower() != '.xls':
            return 'Wrong file format. Only xls files allowed'

        f = tempfile.NamedTemporaryFile(
            delete=False, suffix=".xls", dir='data/tmp/')
        f.write(excel.file.read())
        f.close()
        print(f.name.split('/').pop())
        file_path = 'data/tmp/' + f.name.split('/').pop()
        analyze = analyze_xls(file_path)
        analyze['file'] = f.name.split('/').pop()
        #simport_xls(file_path, user=current_user()['id'])
        #os.remove(file_path)
        return analyze
    elif(request.forms['type'] == 'import'):
        file_path = 'data/tmp/' + request.forms['name']
        amount = import_xls(file_path, user=current_user()['id'])
        os.remove(file_path)
        return {'success': True, 'added': amount}
    else:
        file_path = 'data/tmp/' + request.forms['name']
        os.remove(file_path)
        return {'success': True, 'added': 0}



@post('/upload_blob')
def upload_blob():
    upload = request.files.get('blob')
    print(upload.filename)
    row_id = request.forms['sample_id']
    attr = {}

    file_path = 'data/upload/'+upload.filename
    with open(file_path, 'wb') as open_file:
        open_file.write(upload.file.read())

    attr[request.forms['attr']] = '''local/'''+ file_path
    db.update(int(row_id), attr)
    return 'Done'


@route('/protocol/<id>')
def _get_protocol_js(id):
    response.content_type = 'application/javascript'

    template = open('js/protocolTemplate.js', 'r')
    out = template.read()
    template.close()
    out = out % get_protocol_js(int(id))
    #print(out)
    return out






#
# PROTOCOLS #
#

def add_protocol(name, description, settings, instructions, type, user,
	category='', authors='', view=None, edit=None):
    if view == None: view = edit if edit else user
    if edit == None: edit = view
    if description == False: description = "";
    if instructions == False: instructions = "";
    js = protocol.generate_js_protocol(name, description, settings, instructions, type)
    print(js)
    return db.insert(str(view), {'Type': 'Protocol', 'Name': name, 
        'Authors': authors,
        'Protocol.Settings': settings, 'Protocol.JS': js,
        'Protocol.Instructions': instructions, 'Protocol.Type': type})

def get_protocol_js(id):
    return db.get(id)['Protocol.JS']





#
# EXPERIMENTS #
#

def get_experiments():
    groups = current_user()['groups']
    # groups[1] is users own group
    return db.search(str(groups[1]), 'Type = Experiment')

def add_experiment(protocol_id, protocol_name , expr, user=None, group=None):
    if not type(expr) == str: 
        expr = json.dumps(expr)
    
    return db.insert(str(current_user()['id']), {
        'Type':'Experiment',
        'Experiment.json': expr,
        'Protocol name':protocol_name, 
        'Protocol.id': protocol_id,
        'Author': current_user()['name'],
        'Start Date': datetime.now().strftime("%d.%m.%Y %H:%M:%S"), 
        'Finished':'',
        'State': 'ONGOING' })

def get_experiment_json(id):
    return db.get(id)['Experiment.json']




#
# BULK DATA IMPORT
#

def import_from_generator(keys, items, user, view=None, edit=None):
    if view == None: view = edit if edit else user
    if edit == None: edit = view
   
    all_keys = [k[1:] if k.startswith('%') else k for k in keys]
    merge_keys = [k[1:] for k in keys if k.startswith('%')]
    
    db.insert_multiple(str(view), all_keys, items, merge=merge_keys, edit=edit)



def import_xls(xls_path, user, view=None, edit=None):
    print('Importing %s...' % xls_path)
    book = xlrd.open_workbook(xls_path)
    sheet = book.sheet_by_index(0)

    def items():
        for r in range(1, sheet.nrows):
            row = sheet.row(r)
            fields = [None] * len(row)
            for c, cell in enumerate(row):
                if cell.ctype == 2:
                    text = str(cell.value)
                    fields[c] = text[:-2] if text[-2:] == '.0' else text
                elif cell.ctype == 3: fields[c] = str(datetime(
                    *xlrd.xldate_as_tuple(cell.value, book.datemode)).date())
                else: fields[c] = cell.value
            yield fields

    import_from_generator(sheet.row_values(0), items(), user, view, edit)
    return sheet.nrows-1


def import_tsv(tsv_path, user, view=None, edit=None):
    print('Importing %s...' % tsv_path)
    tsv = open(tsv_path)
    headers = [h.strip() for h in next(tsv)[:-1].split('\t')]
    
    def items():
        for line in tsv:
            cols = line[:-1].split('\t')
            if len(cols) != len(headers): continue
            yield cols
    
    import_from_generator(headers, items(), user, view, edit)
    tsv.close()

             
def analyze_xls(xls_path):
    print('Analyzing %s...' % xls_path)
    book = xlrd.open_workbook(xls_path)
    sheet = book.sheet_by_index(0)

    return {'Attributes': sheet.row_values(0), 'Amount': sheet.nrows-1}




#
# MOCK DATA
#

def populate_mocks():
    public_protocols = add_group('Public protocols')

    # Create user accounts for the demo.
    visakorpi_lab = add_group('IBT - Visakorpi lab')
    kati = add_user('demo', 'demo', 'demo')
    user_join_group(kati, visakorpi_lab)
    
    # Import Kati's primer data.
    data_root = '/crypt/'
    import_xls(data_root+'KW_primers_1.xls', user=kati, edit=visakorpi_lab)
    import_xls(data_root+'KW_primers_2.xls', user=kati, edit=visakorpi_lab)
    # import_tsv(data_root+'PC_patients.tsv', user=kati, edit=visakorpi_lab)
    # import_xls(data_root+'PC_tissues.xls', user=kati, edit=visakorpi_lab)
    #import_tsv(data_root+'PC_patients_small.tsv', user=kati, edit=visakorpi_lab)
    #import_xls(data_root+'PC_tissues_small.xls', user=kati, edit=visakorpi_lab)
    #import_xls(data_root+'PC_sequencing.xls', user=kati, edit=visakorpi_lab)
    
    # Add example protocols
    patient_to_tissue = add_protocol(user=kati, view=public_protocols, edit=kati, type="Protocol",
        **protocol.patient_to_tissue_example)
    tissue_to_dna = add_protocol(user=kati, view=public_protocols, edit=kati, type="Protocol",
        **protocol.tissue_to_dna_extract_example) 
    table_to_dna = add_protocol(user=kati, view=public_protocols, edit=kati, type="Protocol",
        **protocol.table_to_dna_extract_example)
    long_protocol = add_protocol(user=kati, view=public_protocols, edit=kati, type="Protocol",
        **protocol.long_protocol_example)




#
# INITIALIZATION OF THE WSGI APPLICATION
#

bottle.debug(True)

def wsgi_app():
    return bottle.default_app()

if __name__ == '__main__':
	if len(sys.argv) == 2 and sys.argv[1] == 'production':
		db.enable_logging('/crypt/history.log')
		port = 8100
	else:
		db.enable_logging('/home/ltmaan/test2/history.log')
		port = 8080
		#populate_mocks()
	bottle.run(app=wsgi_app(), host='localhost', port=port)